#-*-coding:utf8-*-
import numpy as np
import os
import codecs
import operator
import cmath

import matplotlib.pyplot as plt
from pylab import *  # 支持中文

#得到电影详情
def get_item_info(input_file):
    #首先判断文件是否存在
    if not os.path.exists(input_file):
        print("文件不存在")
        return {}

    #存储电影信息的字典
    item_info={}

    #按行读取文件，并处理行中非 utf-8 的字符
    fp = codecs.open(input_file, 'r',encoding= u'utf-8',errors='ignore')
    for line in fp:
        #把每一行按逗号切割
        item = line.strip().split(',')
        if len(item) < 3:
            continue
        elif len(item) == 3:
            itemid, year, title = item[0],item[1],item[2]
        #处理电影名中出现逗号的情况
        elif len(item)>3:
          itemid = item[0]
          year = item[1]
          title = ",".join(item[2:-1])
        item_info[itemid] = [year,title]
    fp.close()
    print ("item_info:", item_info)
    return item_info


#获得用户对每一部电影的平均评分
def get_ave_score(input_file):
    # 首先判断文件是否存在
    if not os.path.exists(input_file):
        print("文件不存在")
        return {}

    record_dict = {} #存储中间变量
    score_dict = {} #存储结果
    #按行读取文件，并处理行中非 utf-8 的字符
    fp = codecs.open(input_file, 'r',encoding= u'utf-8',errors='ignore')
    i = 0
    for line in fp:
        print(i)
        i += 1
        #把每一行按空格隔开
        item = line.strip().split(' ')
        if len(item) < 4:
            continue

        userid,itemid,rating = item[0],item[1],item[2]
        if itemid not in record_dict:
            #先检查电影id是否在字典中，如若没有找到，将该电影的评分人数和分数都初始化为0
            record_dict[itemid] = [0,0]
        record_dict[itemid][0] += 1  #列表的第一个元素累加人数
        record_dict[itemid][1] += int(rating) # 列表的第二个元素累加评分
    print ("record_dict:",record_dict)
    fp.close()
    #得到用户对每部电影的平均分,并保留3位小数
    for itemid in record_dict:
        score_dict[itemid] = round(record_dict[itemid][1]/record_dict[itemid][0],3)
    print ("score_dict:", score_dict)
    return score_dict

#获取训练数据样本
def get_train_data(input_file):
    if not os.path.exists(input_file):
        print("文件不存在！")
        return []
    score_dict = get_ave_score(input_file)
    pos_dict = {} #正样本,得分大于等于4分
    neg_dict = {}#负样本，得分小于4分
    train_data = [] #用于训练样本的输出
    score_thr = 4.0

    fp = codecs.open(input_file, 'r', encoding=u'utf-8', errors='ignore')
    i = 0
    for line in fp:
        print(i)
        i = i +1
        item = line.strip().split(' ')
        if len(item) < 4:
            continue
        userid, itemid, rating = item[0],item[1],item[2]
        #初始化正负样本
        if userid not in pos_dict:
            pos_dict[userid] = []
        if userid not in neg_dict:
            neg_dict[userid] = []
        #如果该影片评分大于等于4.0，则认为是正样本,存储的数据结构为一个元组
        if int(rating) >= score_thr:
            pos_dict[userid].append((itemid,1))
        else:
            #如果该影片评分小于4分，则得到该电影的平均得分，如果该影片没有被评分过，则返回0分
            score = score_dict.get(itemid,0)
            #将平均评分追加到负样本
            neg_dict[userid].append((itemid,score))
    fp.close()

    #正负样本的数量均衡以及负采样
    for userid in pos_dict:
        #计算每个userid的正负训练样本(评分的影片)的数目,得到较小值
        data_num = min(len(pos_dict[userid]), len(neg_dict.get(userid,[])))
        if data_num > 0:
            #用三元组存储正样本的用户id，影片id,评分，并添加到训练样本的输出结果
            train_data += [(userid,zuhe[0],zuhe[1]) for zuhe in pos_dict[userid]][:data_num]
        else:
          continue

        #对userid所对应的负样本的数据按照得分进行list逆序排序，取训练样本个数个排序结果，使正负样本数目相等
        sorted_neg_list = sorted(neg_dict[userid], key=lambda element:element[1], reverse = True)[:data_num]
        #用三元组存储负样本的用户id，影片id,评分，并添加到训练样本的输出结果，需要注意的是负样本评分全部改为0
        train_data += [(userid,zuhe[0],0) for zuhe in sorted_neg_list]

    print("train_data:", train_data)
    return train_data



#lfm 模型训练函数,参数分别是
# 训练样本，uesrvec（itemvec）的长度或者说维度，正则化的参数，模型的学习率，模型迭代的次数
def lfm_train(train_data,F,alpha,beta,step):

    user_vec = {} #用户向量
    item_vec = {} #影片向量
    delta_list = [] #用于求均方根误差

    #每一次迭代的时候，从训练样本中获取每一个训练的实例
    for step_index in range(step):
        for data_instance in train_data:
            #每一个训练实例都是由 userid,itemid,label的三元组组成
            userid, itemid, label = data_instance
            #初始化，将userid对应的vecter与itemid 对应的vecter 分别初始化为长度为F的向量，按照标准分布取初值
            if userid not in user_vec:
                user_vec[userid] = init_model(F)
            if itemid not in item_vec:
                item_vec[itemid] = init_model(F)

        #模型迭代，对应于公式中损失函数对P Q 的偏导数计算，这里是求label与模型迭代之间的差
        delta = label - model_predict(user_vec[userid],item_vec[itemid])

        tmp = 0
        #对uservec的每一个维度，即f，按照梯度下降的方法求解，注意这里合并的负号
        for index in range(F):
            #括号里面是损失函数对Puf求偏导得到答案的负号,beta为学习率
            user_vec[userid][index] += beta*(delta*item_vec[itemid][index] - alpha*user_vec[userid][index])
            #同理可以得到损失函数对Qif求偏导的结果
            item_vec[itemid][index] += beta*(delta*user_vec[userid][index] - alpha*item_vec[itemid][index])

            #求均方根误差
            tmp += math.pow(delta,2)

        delta_list.append(cmath.sqrt(tmp/F))
        #将学习率进行衰减，目的是在每一轮迭代的时候，在参数更加接近于收敛的时候，能让参数变换的慢一点
        beta = beta*0.9
    # print("user_vec:",user_vec)
    # print("item_vec:",item_vec)

    print("delta_list:",delta_list)
    #画出RMSE变化曲线
    mpl.rcParams['font.sans-serif'] = ['SimHei']
    names = ['5', '10', '15', '20', '25']
    x = range(step)
    # y = [0.855, 0.84, 0.835, 0.815, 0.81]
    # delta_list = [0.86, 0.85, 0.853, 0.849, 0.83]
    # plt.plot(x, y, 'ro-')
    # plt.plot(x, y1, 'bo-')
    # pl.xlim(-1, 11) # 限定横轴的范围
    # pl.ylim(-1, 110) # 限定纵轴的范围
    plt.plot(x, delta_list, marker='o', mec='r', mfc='w', label=u'RMSE')
    plt.legend()  # 让图例生效
    # plt.xticks(x, names, rotation=45)
    plt.margins(0)
    plt.subplots_adjust(bottom=0.15)
    plt.xlabel(u"time(s)邻居")  # X轴标签
    plt.ylabel("RMSE")  # Y轴标签
    plt.title("A simple plot")  # 标题

    plt.show()

    return user_vec,item_vec


#初始化函数
def init_model(vector_len):
     #按照标准的正太分布进行初始化
     return np.random.randn(vector_len)


#得到uservector 与 itemvector 之后，模型对它们的预测值
#返回值为一个数字，表示user_vec 和 item_vec 距离的远近，也就是模型得出的推荐的强度
def model_predict(user_vector,item_vector ):
    #用cos三角函数来表示两个向量的欧式距离远近
    res = np.dot(user_vector,item_vector)/(np.linalg.norm((user_vector)*np.linalg.norm(item_vector)))
    return res


#训练模型
def model_train_process():
    train_data = get_train_data("../data/netflix_test.txt")
    user_vec, item_vec = lfm_train(train_data,20,0.001,0.1,50)
    print(user_vec["305344"])
    # print(item_vec["7244"])
    # #测试推荐效果
    # recom_result = give_recom_result(user_vec,item_vec,'305344')
    # ana_recom_result(train_data,'305344',recom_result)

#根据训练得到的结果做推荐
def give_recom_result(user_vec, item_vec, userid):
     fix_num = 10 #给每一个用户10个推荐结果
     if userid not in user_vec:
         return []
     #存储每一个item与user_vec 之间的距离
     record = {}
     recom_list = []
     user_vector = user_vec[userid]
     #计算每一个userid与itemid对应的向量之间的距离
     for itemid in item_vec:
         item_vector = item_vec[itemid]
     #计算欧式距离
         res = np.dot(user_vector, item_vector)/(np.linalg.norm((user_vector) * np.linalg.norm(item_vector)))
     #将结果存储到之前定义的字典中
         record[itemid] = res
     #对结果进行排序
     for zuhe in sorted(record.iteritems(), key = operator.itemgetter(1), reverse = True)[:fix_num]:
         itemid = zuhe[0]
         score = round(zuhe[1],3) #将推荐得分保留3位有效数字
         recom_list.append((itemid,score))# 存储结果
     print("recom_list:",recom_list)
     return recom_list


#对推荐结果分析 参数分别是 用户之前感兴趣的类别，分析推荐结果的用户id，模型给出的推荐结果
def ana_recom_result(train_data, userid, recom_list):
    item_info = get_item_info("../data/movie_titles.txt") #得到电影数据，用于结果分析
    #分析用户在之前的训练样本中对哪一些item进行过喜欢的操作
    print("============训练数据=============")
    for data_instance in train_data:
        tmp_userid, itemid,label = data_instance
        if tmp_userid==userid and label == 1:
           print (item_info[itemid])
    #对于得到的推荐结果，同样也打印出来
    print("============推荐结果============")
    for zuhe in recom_list:
        print(item_info[zuhe[0]])


if __name__ == "__main__":
    model_train_process()
    #get_item_info("../data/movie_titles.txt")
    # get_ave_score("../data/netflix_train.txt")
    #get_train_data("../data/netflix_train.txt")




